//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using Microsoft.Research.ScientificWorkflow;

namespace Microsoft.Research.ScientificWorkflow.TridentModel
{
    #region using derivatives
    using System;
    using System.Collections.Generic;
    using System.Text;
    using Microsoft.Research.DataLayer;
    using System.Reflection;
    using System.Globalization;
    using System.ComponentModel;
    using TridentAPI;
    #endregion

    /// <summary>
    /// A model describing the meta-data associated with a workflow parameter
    /// of a workflow activity. This data includes whether it is an input or
    /// output paramter, as well as the name, label, and description attached
    /// to that parameter.
    /// </summary>
    [Serializable()]
    public class ParameterDescriptionModel : INotifyPropertyChanged
    {
        #region private variables

        /// <summary>
        /// The Unique id for the parameter.
        /// </summary>
        private Guid id = Guid.Empty;

        /// <summary>
        /// Name of the property as in the custom attribute.
        /// </summary>
        private string name;

        /// <summary>
        /// Name of the property as in the class.
        /// </summary>
        private string propertyName;

        /// <summary>
        ///  Description of the parameter.
        /// </summary>
        private string description;

        /// <summary>
        /// Datatype of the parameter.
        /// </summary>
        private string dataType;

        /// <summary>
        /// Value of the parameter.
        /// </summary>
        private object value;

        /// <summary>
        ///  A flag which signifies whether the parameter is mandatory.
        /// </summary>
        private bool isMandatory;


        /// <summary>
        /// A flag which specifies if the parameter is mandatory for the workflow. 
        /// </summary>
        private bool isMandatoryForWorkflow;

        /// <summary>
        /// A flag which signifies whether the parameter is a input parameter.
        /// </summary>
        private bool isInputParameter;

        /// <summary>
        /// A flag which signifies whether the parameter is a output parameter.
        /// </summary>
        private bool isOutputParameter;

        /// <summary>
        /// A flag which specifies whether the parameter is databound to another parameter.
        /// </summary>
        private bool isDatabound;

        /// <summary>
        /// 
        /// </summary>
        private string label;

        /// <summary>
        /// A flag which indicates whether the value is valid.
        /// </summary>
        private bool isValueInvalid;

        /// <summary>
        /// Output label of the parameter.
        /// </summary>
        private string outputLabel;

        /// <summary>
        /// Output type of the parameter.
        /// </summary>
        private string outputType;

        /// <summary>
        /// Falg which identifies output as Isfinal or not.
        /// </summary>
        private bool isFinal;

        #endregion

        #region Constructor

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="source">Activity parameter object.</param>
        public ParameterDescriptionModel(ActivityParameter source)
        {
            #region Validation of Arguments
            if (null == source)
            {
                throw new ArgumentNullException("source");
            }
            #endregion

            if (source != null)
            {
                // Set the property Name of the Parameter.
                this.propertyName = source.ParameterClass;

                // Set the Name of the Parameter.
                this.name = source.Name;

                // Set the user friendly Name of the Parameter.
                this.label = source.Label;

                // Set the description of the Parameter.
                this.description = source.Description;

                // Set the directions.
                if (source.Direction == DirectionType.Input)
                {
                    this.isInputParameter = true;
                    this.isOutputParameter = false;
                }
                else
                {
                    this.isInputParameter = false;
                    this.isOutputParameter = true;
                }

                // Set datatype.
                this.dataType = source.Type;

                // Set whether the parameter is mandatory or not.
                this.isMandatory = source.Required;

                // Set the default value of the parameter if present.
                if (!string.IsNullOrEmpty(source.DefaultValue))
                {
                    this.Value = source.DefaultValue;
                }

                StringBuilder automationIdBuilder = new StringBuilder();
                automationIdBuilder.Append(source.Activity.Name);
                automationIdBuilder.Append("_");
                automationIdBuilder.Append(this.name);
                this.AutomationId = automationIdBuilder.ToString();
            }
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="metadata">Property metadata object.</param>
        public ParameterDescriptionModel(ParameterDescriptionModel metadata)
        {
            #region Validation of Arguments
            if (null == metadata)
            {
                throw new ArgumentNullException("metadata");
            }
            #endregion

            if (metadata != null)
            {
                // Set the property Name of the Parameter.
                this.propertyName = metadata.PropertyName;

                // Set the Name of the Parameter.
                this.name = metadata.Name;

                // Set the user friendly Name of the Parameter.
                this.label = metadata.Label;

                // Set the description of the Parameter.
                this.description = metadata.Description;

                // Set the directions.
                if (metadata.IsInputParameter)
                {
                    this.isInputParameter = true;
                    this.isOutputParameter = false;
                }
                else
                {
                    this.isInputParameter = false;
                    this.isOutputParameter = true;
                }

                // Set datatype.
                this.dataType = metadata.DataType;

                this.isDatabound = metadata.isDatabound;

                this.isMandatoryForWorkflow = metadata.isMandatoryForWorkflow;

                this.isMandatory = metadata.IsMandatory;

                this.PropertyPaneDisplayName = metadata.PropertyPaneDisplayName;


                if (null != metadata.value)
                {
                    if (metadata.isDatabound)
                    {
                        this.value = metadata.value;
                    }
                    else
                    {
                        this.Value = metadata.value.ToString();
                    }
                }

                this.AutomationId = metadata.AutomationId;
            }
        }

        /// <summary>
        /// Create the ParameterDescriptionModel based on the property info obtained through reflection.
        /// </summary>
        /// <param name="paramPropInfo">
        /// Property info.
        /// </param>
        /// <param name="paramType">
        /// The type of parameter. The parameter type passed should not be In-Out. In case of InOut parameter, create 2 ParameterDescriptionModels,
        /// one for input and one for output.
        /// </param>
        /// <param name="isMandatory">
        /// Indicates whether the parameter is mandatory or not.
        /// </param>
        public ParameterDescriptionModel(
            PropertyInfo parameterPropertyInfo,
            System.Workflow.ComponentModel.Activity activity,
            ParamType parameterType,
            bool isMandatory)
        {
            if (ParamType.InOutParam == parameterType)
            {
                throw new InvalidEnumArgumentException("paramType", 2, typeof(ParamType));
            }

            // Set whether Input Param.
            this.isInputParameter = (ParamType.InputParam == parameterType);
            this.isOutputParameter = (ParamType.OutputParam == parameterType);
            this.isMandatory = isMandatory;

            this.dataType = parameterPropertyInfo.PropertyType.FullName;
            this.id = Guid.Empty;

            object objValue = parameterPropertyInfo.GetGetMethod().Invoke(activity, null);
            if (objValue != null)
            {
                this.Value = objValue.ToString();
            }

            this.propertyName = parameterPropertyInfo.Name;

            // Check to see if the value is data bound
            FieldInfo field = activity.GetType().GetField(parameterPropertyInfo.Name + "Property");
            if (null == field)
            {
                field = activity.GetType().BaseType.GetField(parameterPropertyInfo.Name + "Property");
            }

            if (field != null)
            {
                System.Workflow.ComponentModel.DependencyProperty depProp = (System.Workflow.ComponentModel.DependencyProperty)field.GetValue(activity);
                if (activity.IsBindingSet(depProp))
                {
                    this.IsDatabound = true;
                    System.Workflow.ComponentModel.ActivityBind activityBind = activity.GetBinding(depProp);
                    DatabindModel databinding = new DatabindModel(activityBind.Name, activityBind.Path);
                    this.Value = databinding;
                }
            }

            this.name = parameterPropertyInfo.Name;

            object[] attsNameLabel = parameterPropertyInfo.GetCustomAttributes(typeof(NameAttribute), false);
            if (0 == attsNameLabel.Length)
            {
                this.label = parameterPropertyInfo.Name;
            }
            else
            {
                this.label = ((NameAttribute)attsNameLabel[0]).Label;
            }

            // Set the name of the property from the propertiInfo.
            this.propertyName = parameterPropertyInfo.Name;

            StringBuilder automationIdBuilder = new StringBuilder();
            automationIdBuilder.Append(activity.Name);
            automationIdBuilder.Append("_");
            automationIdBuilder.Append(this.name);
            this.AutomationId = automationIdBuilder.ToString();
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="name">Name for the Parameter.</param>
        /// <param name="description">Description for the Parameter.</param>
        /// <param name="label">Label for the Parameter.</param>
        /// <param name="isInput">Whether input parameter?</param>
        /// <param name="isOutput">Whether output parameter?</param>
        /// <param name="isMandatory">Whether required parameter?</param>
        public ParameterDescriptionModel(
            string name,
            string label,
            string description,
            string dataType,
            bool isInput,
            bool isOutput,
            bool isMandatory)
        {
            #region Validation of Arguments
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            if (string.IsNullOrEmpty(dataType))
            {
                throw new ArgumentNullException("dataType");
            }
            #endregion

            // Set the Name of the Parameter.
            this.name = name;

            // Set the property Name of the Parameter.
            this.propertyName = name;

            // Set the user friendly Name of the Parameter.
            this.label = label;

            // Set the description of the Parameter.
            this.description = description;

            // Set datatype.
            this.dataType = dataType;

            // Set the directions.
            this.isInputParameter = isInput;
            this.isOutputParameter = isOutput;

            // Required parameter.
            this.isMandatory = isMandatory;
        }



        #endregion

        #region public properties

        /// <summary>
        /// The Unique id for the parameter.
        /// </summary>
        public Guid Id
        {
            get
            {
                return id;
            }
        }

        /// <summary>
        /// Name of the parameter as in the class.
        /// </summary>
        public string PropertyName
        {
            get
            {
                return propertyName;
            }
        }

        /// <summary>
        /// Name of the parameter as in attribute.
        /// </summary>
        public string Name
        {
            get
            {
                return this.name;
            }
        }

        /// <summary>
        /// Name of the parameter.
        /// </summary>
        public string Label
        {
            get
            {
                return this.label;
            }
        }

        /// <summary>
        /// Description of the parameter.
        /// </summary>
        public string Description
        {
            get
            {
                return description;
            }
        }

        /// <summary>
        /// Datatype of the parameter.
        /// </summary>
        public string DataType
        {
            get
            {
                return dataType;
            }
        }

        /// <summary>
        /// Value of the parameter.
        /// </summary>
        public object Value
        {
            get
            {
                return value;
            }
            set
            {
                if (value != this.value)
                {
                    this.value = value;
                    this.OnPropertyChanged("Value");
                }
            }
        }

        /// <summary>
        /// A flag which signifies whether the parameter is mandatory.
        /// </summary>
        public bool IsMandatory
        {
            get
            {
                return this.isMandatory;
            }

            set
            {
                if (value != this.isMandatory)
                {
                    this.isMandatory = value;
                    this.OnPropertyChanged("IsMandatory");
                }
            }
        }

        /// <summary>
        /// Gets or Sets a flag which specifies if the parameter is mandatory for the workflow execution. 
        /// </summary>
        public bool IsMandatoryForWorkflow
        {
            get
            {
                return this.isMandatoryForWorkflow;
            }
            set
            {
                this.isMandatoryForWorkflow = value;
                this.OnPropertyChanged("IsMandatoryForWorkflow");
                this.OnPropertyChanged("IsNotMandatoryForWorkflow");
            }
        }

        /// <summary>
        /// A flag which signifies whether the parameter is mandatory for the workflow execution.
        /// </summary>
        public bool IsNotMandatoryForWorkflow
        {
            get
            {
                return !this.isMandatoryForWorkflow;
            }
        }

        /// <summary>
        /// A flag which signifies whether the parameter is a input parameter.
        /// </summary>
        public bool IsInputParameter
        {
            get
            {
                return isInputParameter;
            }
        }

        /// <summary>
        /// A flag which signifies whether the parameter is a output parameter.
        /// </summary>
        public bool IsOutputParameter
        {
            get
            {
                return isOutputParameter;
            }
        }

        /// <summary>
        /// A flag which specifies whether the parameter is databound to another parameter.
        /// </summary>
        public bool IsDatabound
        {
            get
            {
                return isDatabound;
            }
            set
            {
                if (value != this.isDatabound)
                {
                    this.isDatabound = value;
                    this.OnPropertyChanged("IsDatabound");
                }
            }
        }

        /// <summary>
        /// Gets or sets the automation id.
        /// </summary>
        /// <value>The automation id.</value>
        public string AutomationId
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flag which indicates whether the value is valid. To be accessed only after it is loaded in the property pane.
        /// Will not hold a proper value before loading.
        /// </summary>
        public bool IsValueInvalid
        {
            get
            {
                return this.isValueInvalid;
            }
            set
            {
                this.isValueInvalid = value;
                this.OnPropertyChanged("IsValueInvalid");
            }
        }

        /// <summary>
        /// Gets or Sets the name of the activity in which this activity is present. Used only for naming in the property list.
        /// </summary>
        public string PropertyPaneDisplayName { get; set; }

        /// <summary>
        /// Gets or sets the output label.
        /// </summary>
        /// <value>The output label.</value>
        public string OutputLabel
        {
            get
            {
                return this.outputLabel;
            }
            set
            {
                this.outputLabel = value;
                this.OnPropertyChanged("OutputLabel");
            }
        }

        /// <summary>
        /// Gets or sets the type of the output.
        /// </summary>
        /// <value>The type of the output.</value>
        public string OutputType
        {
            get
            {
                return this.outputType;
            }
            set
            {
                this.outputType = value;
                this.OnPropertyChanged("OutputType");
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is final.
        /// </summary>
        /// <value><c>true</c> if this instance is final; otherwise, <c>false</c>.</value>
        public bool IsFinal
        {
            get
            {
                return this.isFinal;
            }
            set
            {
                this.isFinal = value;
                this.OnPropertyChanged("IsFinal");
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Update the value of the parameter based on the assignment.
        /// </summary>
        /// <param name="assignment"></param>
        public void UpdateValue(ParameterAssignment assignment)
        {
            #region Validation of Arguments
            if (null == assignment)
            {
                throw new ArgumentNullException("assignment");
            }
            #endregion

            if (assignment != null)
            {
                this.Value = assignment.CurrentValue;

                this.UpdateAssginmentValue(assignment);
            }
        }

        /// <summary>
        /// Updates the databindings of the object.
        /// </summary>
        /// <param name="assignment">The assignment object.</param>
        public void UpdateBindings(ParameterAssignment assignment)
        {
            #region Validation of Arguments
            if (null == assignment)
            {
                throw new ArgumentNullException("assignment");
            }
            #endregion

            if (assignment != null)
            {
                // If databound then set the binding.
                if (assignment.IsBound)
                {
                    ParameterAssignment sourceAssignment = assignment.BoundTo;
                    if (sourceAssignment != null)
                    {
                        // Retreive the instance name and the property to be bound to.
                        string sourceActivityName = sourceAssignment.Instance.Name;
                        string sourcePropertyName = assignment.BoundToPath;

                        // create a databind model.
                        this.IsDatabound = true;
                        DatabindModel dataBinding = new DatabindModel(sourceActivityName, sourcePropertyName);
                        this.Value = dataBinding;
                    }
                }

                this.UpdateAssginmentValue(assignment);
            }
        }

        /// <summary>
        /// Create a data binding.
        /// </summary>
        /// <param name="sourceActivityName">The source activity name.</param>
        /// <param name="SourcePropertyName">The source property to be bound to.</param>
        /// <returns>Success or failure.</returns>
        public bool CreateBindings(string sourceActivityName, string sourcePropertyName)
        {
            #region Validation of Arguments
            if (string.IsNullOrEmpty(sourceActivityName))
            {
                throw new ArgumentNullException("sourceActivityName");
            }

            if (string.IsNullOrEmpty(sourcePropertyName))
            {
                throw new ArgumentNullException("sourcePropertyName");
            }
            #endregion

            // Check if a binding already exists.
            if (!this.IsDatabound)
            {
                // Create a binding to the parameter model.
                this.IsDatabound = true;
                DatabindModel binding = new DatabindModel(sourceActivityName, sourcePropertyName);
                this.Value = binding;
                return true;
            }

            return false;
        }

        /// <summary>
        /// The string representation of the object. Return the display name. Used in automation.
        /// </summary>
        /// <returns>The string representation of the object.</returns>
        public override string ToString()
        {
            if (!string.IsNullOrEmpty(this.PropertyPaneDisplayName))
            {
                return this.PropertyPaneDisplayName;
            }
            return base.ToString();
        }

        /// <summary>
        /// Updates the assginment value.
        /// </summary>
        /// <param name="assignment">The parameter assignment.</param>
        private void UpdateAssginmentValue(ParameterAssignment assignment)
        {
            if (assignment != null)
            {
                this.IsMandatoryForWorkflow = assignment.RequiredForWorkflow;

                if (this.IsOutputParameter)
                {
                    this.OutputLabel = assignment.OutputLabel;
                    this.OutputType = assignment.Type;
                    this.IsFinal = assignment.IsFinal;
                }
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Fired on property changed.
        /// </summary>
        [field: NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Fired on property changed.
        /// </summary>
        /// <param name="changedProperty">
        /// changedProperty.
        /// </param>
        private void OnPropertyChanged(string changedProperty)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(changedProperty));
            }
        }

        #endregion

        /// <summary>
        /// Sort the input parameters based on whether they are mandatory or not.
        /// </summary>
        /// <param name="first">The first model to be compared.</param>
        /// <param name="second">The model to be compared with the first.</param>
        /// <returns>0 if the two are equal, 1 if the first is greater than the second, -1 otherwise.</returns>
        public static int SortInputParameters(ParameterDescriptionModel first, ParameterDescriptionModel second)
        {
            int result = 0;
            int priorityOfFirst = GetPriorities(first);
            int priorityOfSecond = GetPriorities(second);

            if (priorityOfFirst > priorityOfSecond)
            {
                result = -1;
            }
            else if (priorityOfFirst < priorityOfSecond)
            {
                result = 1;
            }
            else
            {
                int compareResult = string.Compare(first.label, second.label, StringComparison.OrdinalIgnoreCase);
                if (compareResult > 0)
                {
                    result = 1;
                }
                else if (compareResult < 0)
                {
                    result = -1;
                }
            }
            return result;
        }

        /// <summary>
        /// Assign priorities to the parameter.
        /// </summary>
        /// <param name="parameter">The parameter model.</param>
        /// <returns>The priority of the parameter.</returns>
        private static int GetPriorities(ParameterDescriptionModel parameter)
        {
            int result = 0;
            if (parameter.IsMandatory)
            {
                result += 1;
            }
            if (parameter.IsMandatoryForWorkflow)
            {
                result += 2;
            }
            return result;
        }
    }
}
